/*
 * TazioRootSet.java   0.0.1    4 Nov 2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.transactions.model.impl;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import org.mentalsmash.tazio.transactions.control.TazioTransaction;
import org.mentalsmash.tazio.transactions.control.TransactionStateException;
import org.mentalsmash.tazio.transactions.model.NoSuchPropertyException;
import org.mentalsmash.tazio.transactions.model.Query;
import org.mentalsmash.tazio.transactions.model.RootSet;
import org.mentalsmash.tazio.transactions.model.RootSetUpdate;
import org.mentalsmash.tazio.transactions.model.SetSnapshot;
import org.mentalsmash.tazio.transactions.model.TransactionalObject;
import org.mentalsmash.tazio.transactions.model.TransactionalOperation;
import org.mentalsmash.tazio.transactions.model.identifiers.OID;
import org.mentalsmash.tazio.transactions.user.RollbackException;
import org.mentalsmash.tazio.transactions.user.TransactionStatus;
import org.mentalsmash.tazio.transactions.user.TransactionalSystemException;
import org.mentalsmash.tazio.transactions.user.TransactionalSystemRuntimeException;
import org.mentalsmash.tazio.transactions.user.UserTransactionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * Implementation of the RootSet interface in Tazio
 * 
 * @author Andrea Reale
 * @version 0.0.1
 */
@Singleton
public class TazioRootSet implements RootSet {

    private static final long serialVersionUID = 3278867182683768373L;
    private final static Logger log = LoggerFactory
	    .getLogger(TazioRootSet.class);
    private final static String[] PROPERTY_NAMES = new String[] { "RootSet" };

    private final Map<OID, TransactionalObject> transactionalObjects;
    private final transient UserTransactionManager tm;

    private long version = TransactionalObject.INITIAL_VERSION;

    @Inject
    public TazioRootSet(UserTransactionManager tm) {
	this.tm = tm;

	Map<OID, TransactionalObject> tempMap = Maps.newHashMap();
	this.transactionalObjects = Collections.synchronizedMap(tempMap);
    }

    /* ****************************************************** */
    /*                           *********** RootSet interface ************** */
    /* ****************************************************** */

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.model.RootSet#containsObject(org.
     * mentalsmash.tazio.transactions.model.identifiers.OID)
     */
    @Override
    public boolean containsObject(OID objectId) {
	return this.transactionalObjects.containsKey(objectId);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.RootSet#doAddObject(org.mentalsmash
     * .tazio.transactions.model.TransactionalObject)
     */
    @Override
    public void doAddObject(TransactionalObject obj) {
	this.transactionalObjects.put(obj.getObjectIdentifier(), obj);

    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.model.RootSet#doRemoveObject(org.
     * mentalsmash.tazio.transactions.model.identifiers.OID)
     */
    @Override
    public OID doRemoveObject(OID toRemove) {
	TransactionalObject removed = this.transactionalObjects
		.remove(toRemove);
	if (removed != null) {
	    return removed.getObjectIdentifier();
	} else {
	    return null;
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.RootSet#getObject(org.mentalsmash
     * .tazio.transactions.model.identifiers.OID)
     */
    @Override
    public TransactionalObject getObject(OID objectId) {
	if(objectId.equals(ROOT_SET_OID)) {
	    return this;
	} else {
	    return this.transactionalObjects.get(objectId);
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mentalsmash.tazio.transactions.model.RootSet#txIterator()
     */
    @Override
    public Iterator<TransactionalObject> txIterator() {
	return this.transactionalObjects.values().iterator();
    }

    /* ****************************************************** */
    /*                        ******** TransactionalSet interface ************** */
    /* ****************************************************** */

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalSet#containsPrimitives
     * ()
     */
    @Override
    public boolean containsPrimitives() {
	return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalSet#doAdd(java.
     * io.Serializable)
     */
    @Override
    public void doAdd(Serializable obj) {
	Preconditions.checkArgument(obj instanceof TransactionalObject,
		"Objects must be instances of TransactionalObject");
	doAddObject((TransactionalObject) obj);

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalSet#doRemove(java
     * .io.Serializable)
     */
    @Override
    public Serializable doRemove(Serializable toRemove) {
	Preconditions.checkArgument(toRemove instanceof TransactionalObject,
		"Objects must be instances of TransactionalObject");
	final OID removed = doRemoveObject(((TransactionalObject) toRemove)
		.getObjectIdentifier());

	if (removed != null) {
	    return toRemove;
	} else {
	    return null;
	}
    }

    /* ****************************************************** */
    /*                        ******** TransactionalObject interface ************** */
    /* ****************************************************** */

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.model.TransactionalObject#
     * getObjectIdentifier()
     */
    @Override
    public OID getObjectIdentifier() {
	return ROOT_SET_OID;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#getPropertyNames
     * ()
     */
    @Override
    public String[] getPropertyNames() {
	return PROPERTY_NAMES;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#getPropertyType
     * (java.lang.String)
     */
    @Override
    public Class<?> getPropertyType(String name) throws NoSuchPropertyException {
	if (name.equals(PROPERTY_NAMES[0])) {
	    return RootSet.class;
	} else {
	    throw new NoSuchPropertyException();
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#getPropertyValue
     * (java.lang.String)
     */
    @Override
    public Serializable getPropertyValue(String name)
	    throws NoSuchPropertyException {
	if (name.equals(PROPERTY_NAMES[0])) {
	    return this;
	} else {
	    throw new NoSuchPropertyException();
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.model.TransactionalObject#
     * getTransactionalInterface()
     */
    @Override
    public Class<?> getTransactionalInterface() {
	return RootSet.class;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#getVersion()
     */
    @Override
    public long getVersion() {
	return this.version;
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.model.TransactionalObject#
     * isReadOnlyProperty(java.lang.String)
     */
    @Override
    public boolean isReadOnlyProperty(String name)
	    throws NoSuchPropertyException {
	return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#setPropertyValue
     * (java.lang.String, java.io.Serializable)
     */
    @Override
    public void setPropertyValue(String name, Serializable value)
	    throws NoSuchPropertyException {
	throw new UnsupportedOperationException("Not supported for RootSet");

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.model.TransactionalObject#setVersion
     * (long)
     */
    @Override
    public void setVersion(long newVersion) {
	Preconditions.checkArgument(newVersion > 0,
		"Version must be a positive number");
	this.version = newVersion;

    }

    /* ****************************************************** */
    /*                           *********** Set<Serializable> interface ************** */
    /* ****************************************************** */

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#add(java.lang.Object)
     */
    @Override
    public boolean add(Serializable e) {
	Preconditions.checkArgument(e instanceof TransactionalObject);
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);
	rootSetSnapshot.add(e);

	RootSetUpdate rsu = new RootSetUpdate(this.version, false);
	rsu.setValue(e);
	tx.logOperation(rsu);
	tx.addToWriteSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return false;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return false;
	    }
	}

	return true;

    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#addAll(java.util.Collection)
     */
    @Override
    public boolean addAll(Collection<? extends Serializable> c) {
	throw new UnsupportedOperationException();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#clear()
     */
    @Override
    public void clear() {
	throw new UnsupportedOperationException();

    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#contains(java.lang.Object)
     */
    @Override
    public boolean contains(Object o) {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	boolean returnValue = rootSetSnapshot.contains(o);

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return false;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return false;
	    }
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#containsAll(java.util.Collection)
     */
    @Override
    public boolean containsAll(Collection<?> c) {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	boolean returnValue = rootSetSnapshot.containsAll(c);

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return false;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return false;
	    }

	}
	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#isEmpty()
     */
    @Override
    public boolean isEmpty() {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	boolean returnValue = rootSetSnapshot.isEmpty();

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return false;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return false;
	    }
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#iterator()
     */
    @Override
    public Iterator<Serializable> iterator() {

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();
	if (!tx.getStatus().equals(TransactionStatus.RUNNING)) {
	    throw new TransactionalSystemRuntimeException(
		    "Iterator of the root set can only be retrieved, in the context of an user started transaction");
	}
	final Iterator<Serializable> toWrap = getUncheckedIterator();
	final TransactionalIterator iterator = new TransactionalIterator(
		this.tm, tx.getTransactionId(), toWrap);
	
	tx.addToReadSet(ROOT_SET_OID);

	return iterator;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#remove(java.lang.Object)
     */
    @Override
    public boolean remove(Object o) {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();
	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	boolean returnValue = rootSetSnapshot.remove(o);

	RootSetUpdate rsu = new RootSetUpdate(this.version, true);
	rsu.setValue(((TransactionalObject) o).getObjectIdentifier());

	tx.logOperation(rsu);
	tx.addToWriteSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return false;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return false;
	    }
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#removeAll(java.util.Collection)
     */
    @Override
    public boolean removeAll(Collection<?> c) {
	throw new UnsupportedOperationException();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#retainAll(java.util.Collection)
     */
    @Override
    public boolean retainAll(Collection<?> c) {
	throw new UnsupportedOperationException();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#size()
     */
    @Override
    public int size() {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	int returnValue = rootSetSnapshot.size();

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return -1;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return -1;
	    }
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#toArray()
     */
    @Override
    public Object[] toArray() {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	Object[] returnValue = rootSetSnapshot.toArray();

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return null;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return null;
	    }
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.Set#toArray(T[])
     */
    @Override
    public <T> T[] toArray(T[] a) {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);

	T[] returnValue = rootSetSnapshot.toArray(a);

	Query rsq = new Query(ROOT_SET_OID, this.version);
	rsq.setPropertyName(PROPERTY_NAMES[0]);

	tx.logOperation(rsq);
	tx.addToReadSet(ROOT_SET_OID);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return null;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return null;
	    }
	}

	return returnValue;
    }

    private boolean assureTransaction() throws TransactionalSystemException {
	TazioTransaction tx = (TazioTransaction) tm.getTransaction();
	Preconditions.checkState(tx.getStatus().equals(
		TransactionStatus.RUNNING)
		|| tx.getStatus().equals(TransactionStatus.NO_TRANSACTION),
		"Invalid transaction status");
	if (tx.getStatus().equals(TransactionStatus.RUNNING)) {
	    return false;
	} else {
	    log.debug("No running transaction found... creating one");
	    tx = (TazioTransaction) tm.begin();
	    return true;
	}

    }

    private Object invokeInTranasction(Method m, Object arg, boolean query) {
	boolean dedicateTxStarted = false;

	try {
	    dedicateTxStarted = assureTransaction();
	} catch (TransactionalSystemException e2) {
	    log.error("FATAL:", e2);
	    throw new TransactionalSystemRuntimeException(e2);
	}

	final TazioTransaction tx = (TazioTransaction) tm.getTransaction();

	// take a snapshot of the RootSet and perform modifications on the
	// snapshot
	tx.takeSnapshot(this);
	SetSnapshot rootSetSnapshot = (SetSnapshot) tx
		.retrieveSnapshot(ROOT_SET_OID);


	Object returnValObject = null;
	try {
	    returnValObject = m.invoke(rootSetSnapshot, arg);
	} catch (IllegalArgumentException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	} catch (IllegalAccessException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	} catch (InvocationTargetException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
	
	TransactionalOperation toLog;
	if (query) {
	    toLog = new Query(ROOT_SET_OID, this.version);
	    toLog.setPropertyName(PROPERTY_NAMES[0]);
	} else {
	    toLog = new RootSetUpdate(this.version, true);
	    ((RootSetUpdate) toLog).setValue(((TransactionalObject) arg)
		    .getObjectIdentifier());
	}
	
	tx.logOperation(toLog);

	if (dedicateTxStarted) {
	    try {
		tx.commit();
	    } catch (RollbackException e1) {
		log.warn("Modification to the RootSet were rolled back", e1);
		return null;

	    } catch (TransactionStateException e1) {
		log.warn("Unexpected exception while mofying the RootSet", e1);
		return null;
	    }
	}

	return returnValObject;
    }

    private Iterator<Serializable> getUncheckedIterator() {
	return new Iterator<Serializable>() {

	    private final Iterator<TransactionalObject> internalIt = transactionalObjects
		    .values().iterator();

	    @Override
	    public boolean hasNext() {
		return internalIt.hasNext();
	    }

	    @Override
	    public Serializable next() {
		return internalIt.next();
	    }

	    @Override
	    public void remove() {
		throw new UnsupportedOperationException();

	    }
	};
    }

}
